package channels4java.stream;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.BindException;
import java.net.ConnectException;
import java.net.InetAddress;
import java.net.NoRouteToHostException;
import java.net.Socket;
import java.net.UnknownHostException;

import channels4java.BehaviourConfiguration;
import channels4java.exceptions.BehaviourChangeException;
import channels4java.exceptions.ChannelCreationException;
import channels4java.exceptions.Channels4JavaException.ErrorType;
import channels4java.exceptions.HandshakeException;
import channels4java.exceptions.MessageCreationException;
import channels4java.exceptions.MessageReceptionException;
import channels4java.exceptions.MessageSendingException;
import channels4java.exceptions.X509CertificateCreationException;
import channels4java.messages.Message;
import channels4java.messages.Message.CommonParameter;
import channels4java.messages.Message.MessageType;
import channels4java.security.X509Certificate;
import channels4java.utils.Base64Codec;

public class CommunicationChannel
{
	/**
	 *This enumeration defines all the available channel behaviours.
	 *
	 *<li><b>LEGACY: </b>this is the default behaviour of all communication Channels and only allow the sending/receiving of messages.
	 *It doesn't protect the data being transmitted and acts like a normal socket but with a high level API.</li>
	 *<li><b>ENCRYPTED: </b> this behaviour is only supported if the Communication Hub (server-side) has a Public Key Certificate (X509).
	 *This behaviour won't check for the certificate revocation with the corresponding Certificate Authority (CA)
	 *of the certificate. The public key imprinted with the certificate is used by the asking channel to exchange a symmetric key 
	 *that will cipher the data sent and received. This method doesn't protected against Man-in-the-middle attacks.</li>
	 *<li><b>SECURED: </b>to implement later... (equivalent to SSL security with certificate revocation check)</li>
	 */
	public static enum ChannelBehaviour
	{
		LEGACY,
		ENCRYPTED,
		SECURED
	}
	
	private Base64Codec base64codec;
	
	private ChannelBehaviour behaviour;
	
	private Socket channel;
	private BufferedReader input;
	private PrintStream output;
	
	private X509Certificate remoteCertificate;
	private X509Certificate localCertificate;
	
	public CommunicationChannel(String host, int port) throws ChannelCreationException
	{
		InetAddress hostAddress;
		try 
		{
			hostAddress = InetAddress.getByName(host);
		}
		catch(UnknownHostException e) 
		{
			throw new ChannelCreationException(ErrorType.UNKNOWN_HOST, "the IP address of the remote host could not be determined", e);
		}
		
		if(port < 0)
		{
			throw new ChannelCreationException(ErrorType.INVALID_PORT, "the specified port number is invalid. Can't be less than zero", null);
		}
		
		try
		{
			this.channel = new Socket(hostAddress, port);
		}
		catch(BindException e) 
		{
			throw new ChannelCreationException(ErrorType.BIND_ERROR, "it was not possible to bind the socket to the local address. Maybe the port is in use", e);
		}
		catch(ConnectException e) 
		{
			throw new ChannelCreationException(ErrorType.CONNECTION_ERROR, "typically the connection was refused remotely (maybe there is no process listening at the given IP/port)", e);
		}
		catch(NoRouteToHostException e) 
		{
			throw new ChannelCreationException(ErrorType.NO_ROUTE_TO_HOST, "typically the remote host cannot be reached because of an intervening firewall, or if an intermediate router is down", e);
		}
		catch(IOException e) 
		{
			throw new ChannelCreationException(ErrorType.INPUT_OUTPUT, "it was not possible to create the channel because of an I/O error", e);
		}
		
		this.behaviour = ChannelBehaviour.LEGACY;
		
		try
		{
			this.executeHandshake();
		}
		catch(HandshakeException e) 
		{
			throw new ChannelCreationException(ErrorType.HANDSHAKE_ERROR, "it was not possible to create the channel due to an error in the handshake process", e);
		}
	}
	
	private void executeHandshake() throws HandshakeException
	{
		try
		{
			this.input = new BufferedReader(new InputStreamReader(this.channel.getInputStream()));
			this.output = new PrintStream(this.channel.getOutputStream(), true);
		}
		catch(IOException e) 
		{
			throw new HandshakeException(ErrorType.INPUT_OUTPUT, "", e);
		}
		
		Message handshake = new Message(MessageType.HANDSHAKE);
		this.output.println(handshake.toString());
		
		try 
		{
			handshake = new Message(this.input.readLine());
		}
		catch(MessageCreationException e) 
		{
			throw new HandshakeException(ErrorType.INVALID_MESSAGE_FORMAT, "the received message is not in the supported format", e);
		}
		catch(IOException e) 
		{
			throw new HandshakeException(ErrorType.INPUT_OUTPUT, "the handshake could not be executed due to an I/O error", e);
		}
		
		if(handshake.getMessageType() == MessageType.HANDSHAKE)
		{
			if(handshake.containsParameter(CommonParameter.SESSION_REJECTED))
			{
				throw new HandshakeException(ErrorType.SESSION_REJECTED, "the session was rejected by the remote communication hub", null);
			}
			else
			{
				if(handshake.containsParameter(CommonParameter.CERTIFICATE))
				{
					try 
					{
						this.remoteCertificate = new X509Certificate(handshake.getParameterValue(CommonParameter.CERTIFICATE));
					}
					catch(X509CertificateCreationException e)
					{
						// the certificate is optional at this stage... If there is an error while getting the certificate then that is not a major problem
					}
				}
			}
		}
		else
		{
			throw new HandshakeException(ErrorType.INVALID_MESSAGE_TYPE, "the returned message is not valid", null);
		}
	}
	
	/*
	 * public interface
	 */
	
	public X509Certificate getRemoteCertificate()
	{
		try 
		{
			return new X509Certificate(this.remoteCertificate.getEncoded());
		} 
		catch(Exception e) 
		{
			return this.remoteCertificate;
		}
	}
	
	public X509Certificate getLocalCertificate()
	{
		try 
		{
			return new X509Certificate(this.localCertificate.getEncoded());
		} 
		catch(Exception e) 
		{
			return this.localCertificate;
		}
	}
	
	public void attachLocalCertificate(String base64Cert) throws X509CertificateCreationException
	{
		this.localCertificate = new X509Certificate(base64Cert);
	}
	
	public void attachLocalCertificate(File certFile) throws X509CertificateCreationException
	{
		this.localCertificate = new X509Certificate(certFile);
	}
	
	public void attachLocalCertificate(InputStream certStream) throws X509CertificateCreationException
	{
		this.localCertificate = new X509Certificate(certStream);
	}
	
	public void changeBehaviour(ChannelBehaviour behaviour, BehaviourConfiguration config) throws BehaviourChangeException
	{
		if(behaviour == null) throw new IllegalArgumentException("invalid behaviour");
		
		
	}
	
	public void sendMessage(String message) throws MessageSendingException
	{
		Message msg = null;
		if(this.base64codec == null) this.base64codec = new Base64Codec();
		
		if(this.behaviour == ChannelBehaviour.LEGACY)
		{
			msg = new Message(MessageType.DATA);
			try 
			{
				msg.addParameter(CommonParameter.PAYLOAD, this.base64codec.encodeBytes(message.getBytes(), Base64Codec.GZIP));
			}
			catch(IOException e) 
			{
				throw new MessageSendingException(ErrorType.INPUT_OUTPUT, "it was not possible to send the message because of an I/O error", e);
			}
		}
		
		this.output.println(msg.toString());
	}
	
	public String receiveMessage() throws MessageReceptionException
	{
		Message receivedMessage = null;
		if(this.base64codec == null) this.base64codec = new Base64Codec();
		
		if(this.behaviour == ChannelBehaviour.LEGACY)
		{
			try
			{
				receivedMessage = new Message(this.input.readLine());
			}
			catch(MessageCreationException e)
			{
				throw new MessageReceptionException(ErrorType.INVALID_MESSAGE_FORMAT, "", e);
			}
			catch(IOException e) 
			{
				throw new MessageReceptionException(ErrorType.INVALID_MESSAGE_FORMAT, "", e);
			}
			
			if(receivedMessage.getMessageType() == MessageType.DATA)
			{
				try 
				{
					return new String(this.base64codec.decode(receivedMessage.getParameterValue(CommonParameter.PAYLOAD)));
				} 
				catch(IOException e) 
				{
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
		}
		
		return null;
	}
	
	// basta ter um certificado do servidor (nao verifica a CA)
	public boolean supportsChannelEncryption()
	{
		return false;
	}
	
	// so se o endpoint tiver dado o seu certificado e a CA que assinou o certificado for conhecida
	public boolean supportsChannelSecurity()
	{
		return false;
	}
}
